package cli

import (
	"flag"
	"io/ioutil"
	"os"
	"strings"
	"time"

	"gitee.com/zawei/yggdrasil/pkg/cmd"
	"gitee.com/zawei/yggdrasil/pkg/config/source"
	"gitee.com/zawei/yggdrasil/third_party/urfave/cli"
	"github.com/imdario/mergo"
)

type cliSource struct {
	opts source.Options
	ctx  *cli.Context
}

func (c *cliSource) Read() (*source.ChangeSet, error) {
	var changes map[string]interface{}
	// directly using app cli flags, to access default values of not specified options
	for _, f := range c.ctx.App.Flags {
		name := f.Names()[0]
		if name != "help" {
			_ = mergo.Map(&changes, toEntry(name, c.ctx.Generic(name))) // need to sort error handling
		}
	}
	if c.ctx.Command != nil {
		for _, f := range c.ctx.Command.Flags {
			name := f.Names()[0]
			if name != "help" {
				_ = mergo.Map(&changes, toEntry(name, c.ctx.Generic(name))) // need to sort error handling
			}
		}
	}
	b, err := c.opts.Encoder.Encode(changes)
	if err != nil {
		return nil, err
	}
	cs := &source.ChangeSet{
		Priority:  source.PriorityCli,
		Format:    c.opts.Encoder.String(),
		Data:      b,
		Timestamp: time.Now(),
		Source:    c.String(),
	}
	cs.Checksum = cs.Sum()

	return cs, nil
}

func toEntry(name string, v interface{}) map[string]interface{} {
	n := strings.ToLower(name)
	keys := strings.FieldsFunc(n, split)
	reverse(keys)
	tmp := make(map[string]interface{})
	for i, k := range keys {
		if i == 0 {
			tmp[k] = v
			continue
		}

		tmp = map[string]interface{}{k: tmp}
	}
	return tmp
}

func reverse(ss []string) {
	for i := len(ss)/2 - 1; i >= 0; i-- {
		opp := len(ss) - 1 - i
		ss[i], ss[opp] = ss[opp], ss[i]
	}
}

func split(r rune) bool {
	return r == '-' || r == '_'
}

func (c *cliSource) Watch() (source.Watcher, error) {
	return nil, nil
}

// Write is unsupported
func (c *cliSource) Write(cs *source.ChangeSet) error {
	return nil
}

func (c *cliSource) String() string {
	return "cli"
}

// NewSource returns a config source for integrating parsed flags from a micro/cli.Context.
// Hyphens are delimiters for nesting, and all keys are lowercased. The assumption is that
// command line flags have already been parsed.
//
// Example:
//      cli.StringFlag{Name: "db-host"},
//
//
//      {
//          "database": {
//              "host": "localhost"
//          }
//      }
func NewSource(opts ...source.Option) source.Source {
	options := source.NewOptions(opts...)

	var ctx *cli.Context

	if c, ok := options.Context.Value(contextKey{}).(*cli.Context); ok {
		ctx = c
	} else {
		// no context
		// get the default app/flags
		app := cmd.App()
		ctx = newContext(app, app.Name, app.Flags, nil)
	}

	return &cliSource{
		ctx:  ctx,
		opts: options,
	}
}

func newContext(app *cli.App, name string, flags []cli.Flag, pCtx *cli.Context) *cli.Context {
	// create flagset
	set := flag.NewFlagSet(name, flag.ContinueOnError)

	// apply flags to set
	for _, f := range flags {
		f.Apply(set)
	}

	// parse flags
	set.SetOutput(ioutil.Discard)
	set.Parse(os.Args[1:])

	// normalise flags
	normalizeFlags(app.Flags, set)

	// create context
	return cli.NewContext(app, set, pCtx)
}

// WithContext returns a new source with the context specified.
// The assumption is that Context is retrieved within an app.Action function.
func WithContext(ctx *cli.Context, opts ...source.Option) source.Source {
	return &cliSource{
		ctx:  ctx,
		opts: source.NewOptions(opts...),
	}
}
